Add referrer with annotations for image source and base image name + digest#2163
Add referrer with annotations for image source and base image name + digest#2163lbussell wants to merge 13 commits into
Conversation
| manifestServiceFactory: CreateManifestServiceFactoryMock( | ||
| localImageDigestResults: [new(baseImageTag, baseImageDigest)]).Object, | ||
| imageCacheService: new ImageCacheService(Mock.Of<ILogger<ImageCacheService>>(), Mock.Of<IGitService>())); | ||
| command.Options.Manifest = Path.Combine(tempFolderContext.Path, "manifest.json"); |
| command.Options.SourceRepoUrl = sourceRepoUrl; | ||
|
|
||
| const string runtimeRelativeDir = "1.0/runtime/os"; | ||
| Directory.CreateDirectory(Path.Combine(tempFolderContext.Path, runtimeRelativeDir)); |
|
|
||
| const string runtimeRelativeDir = "1.0/runtime/os"; | ||
| Directory.CreateDirectory(Path.Combine(tempFolderContext.Path, runtimeRelativeDir)); | ||
| string dockerfileRelativePath = Path.Combine(runtimeRelativeDir, "Dockerfile"); |
| const string runtimeRelativeDir = "1.0/runtime/os"; | ||
| Directory.CreateDirectory(Path.Combine(tempFolderContext.Path, runtimeRelativeDir)); | ||
| string dockerfileRelativePath = Path.Combine(runtimeRelativeDir, "Dockerfile"); | ||
| string dockerfileAbsolutePath = PathHelper.NormalizePath(Path.Combine(tempFolderContext.Path, dockerfileRelativePath)); |
| CreateImage( | ||
| new Platform[] { platform }))); | ||
|
|
||
| File.WriteAllText(Path.Combine(tempFolderContext.Path, command.Options.Manifest), JsonConvert.SerializeObject(manifest)); |
| command.Options.Manifest = Path.Combine(tempFolderContext.Path, "manifest.json"); | ||
|
|
||
| const string runtimeRelativeDir = "1.0/runtime/os"; | ||
| Directory.CreateDirectory(Path.Combine(tempFolderContext.Path, runtimeRelativeDir)); |
|
|
||
| const string runtimeRelativeDir = "1.0/runtime/os"; | ||
| Directory.CreateDirectory(Path.Combine(tempFolderContext.Path, runtimeRelativeDir)); | ||
| string dockerfileRelativePath = Path.Combine(runtimeRelativeDir, "Dockerfile"); |
| const string runtimeRelativeDir = "1.0/runtime/os"; | ||
| Directory.CreateDirectory(Path.Combine(tempFolderContext.Path, runtimeRelativeDir)); | ||
| string dockerfileRelativePath = Path.Combine(runtimeRelativeDir, "Dockerfile"); | ||
| string dockerfileAbsolutePath = PathHelper.NormalizePath(Path.Combine(tempFolderContext.Path, dockerfileRelativePath)); |
| CreateImage( | ||
| new Platform[] { platform }))); | ||
|
|
||
| File.WriteAllText(Path.Combine(tempFolderContext.Path, command.Options.Manifest), JsonConvert.SerializeObject(manifest)); |
| IEnumerable<TagInfo> tagsForDigest = imageArtifactDetails is null ? [] : concreteTags; | ||
|
|
||
| // Log in again to refresh token as it may have expired from a long build | ||
| await ExecuteWithDockerCredentialsAsync( |
There was a problem hiding this comment.
What are the perf consequences of running this for every single image that gets built?
There was a problem hiding this comment.
I'm testing it internally and I'll get back to you.
There was a problem hiding this comment.
This comment is still relevant.
| string? digest = null; | ||
| for (int attempt = 0; attempt <= RetryHelper.MaxRetries && digest is null; attempt++) | ||
| { | ||
| digest = await imageDigestCache.GetLocalImageDigestAsync(tag.FullyQualifiedName, Options.IsDryRun); |
There was a problem hiding this comment.
Include some sort of timing backoff for each attempt.
There was a problem hiding this comment.
I'm going to take a second pass at this method. This is not how we should do retries. Thanks for calling it out.
There was a problem hiding this comment.
This comment is still relevant.
| string repoRoot = _gitService.GetRepoRoot(platform.DockerfilePath); | ||
| labels[ImageBuilderLabels.Dockerfile] = | ||
| PathHelper.NormalizePath(Path.GetRelativePath(repoRoot, platform.DockerfilePath)); |
There was a problem hiding this comment.
Use GitServiceExtensions.GetDockerfileCommitUrl instead?
There was a problem hiding this comment.
This is a workaround for #2165. Additionally, since the source URL is already present in the org.opencontainers.image.source label, including the repo base URL a second time in another label is not necessary. That's why I chose only the Dockerfile path here.
There was a problem hiding this comment.
If we already have org.opencontainers.image.source then why do we need this label?
ad2da9b to
5fe71e3
Compare
| manifestServiceFactory: CreateManifestServiceFactoryMock().Object, | ||
| imageCacheService: new ImageCacheService(Mock.Of<ILogger<ImageCacheService>>(), Mock.Of<IGitService>()), | ||
| orasServiceFactory: orasServiceFactoryMock.Object); | ||
| command.Options.Manifest = Path.Combine(tempFolderContext.Path, "manifest.json"); |
Removes the `--label` build-metadata mechanism from the image build path in preparation for attaching the same metadata as an OCI referrer artifact. The `labels` parameter is dropped from `IDockerService.BuildImage` (and its `DockerService`/`DockerServiceCache` implementations) along with the label-building block in `BuildCommand`. The corresponding label-verification tests are removed and the `BuildImage` mock verifications are updated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Attaches image build metadata (source repo, revision, Dockerfile path, and base image name/digest) to each freshly built and pushed platform image as an OCI referrer artifact whose annotations carry the metadata, rather than baking it into the image as Docker labels. Referrer annotations do not propagate to downstream images and can evolve independently of the image, per #2166. The metadata is recorded using annotation keys in ImageBuilder's own `com.microsoft.imagebuilder.*` namespace rather than the standard `org.opencontainers.image.*` keys. OCI annotations describe the artifact they are placed on; on a referrer artifact the standard keys would describe the referrer itself rather than the subject image, so a custom namespace is used. After pushing a platform, `BuildCommand` builds the annotations inline and attaches them to the pushed image by digest via an `IOrasService` (created from the command's registry credentials) using the new `OciArtifactType.ImageInfoReferrer` artifact type. The `ImageBuilderLabels` constants type is replaced by `ImageBuilderAnnotations`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Aligns the image build metadata annotation keys with the existing `vnd.microsoft.artifact.lifecycle.*` annotations used for end-of-life metadata, moving them from `com.microsoft.imagebuilder.*` to `vnd.microsoft.imagebuilder.*`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The build-metadata referrer carries all of its data in manifest annotations and has no content blob. Using a custom '+json' artifact type wrongly implies a JSON payload, so use the empty OCI artifact type (application/vnd.oci.empty.v1+json) instead, matching the behavior of 'oras attach' with no files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
5fe71e3 to
27b987a
Compare
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR adds an OCI referrer artifact that records ImageBuilder build metadata (as annotations) for each pushed platform image, enabling consumers to discover source/base-image provenance without inheriting labels into downstream images. It also restructures the build flow to build → push → compute metadata → attach referrer per platform so the referrer can be bound to the exact pushed digest.
Changes:
- Add ImageBuilder-specific annotation keys and a new OCI artifact type (
application/vnd.microsoft.imagebuilder.image-info.v1) for the metadata referrer. - Extend Git utilities to reliably locate the repo root (including worktree/submodule
.gitfile scenarios) and use that to record Dockerfile paths relative to repo root. - Refactor
BuildCommandto push each platform immediately after building and then attach the metadata referrer; add unit tests validating referrer attachment behavior.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/ImageBuilder/Oras/OciArtifactType.cs | Adds a constant for the ImageBuilder image-info referrer artifact type. |
| src/ImageBuilder/ImageBuilderAnnotations.cs | Introduces well-defined annotation keys used on the referrer manifest to describe the subject image. |
| src/ImageBuilder/IGitService.cs | Adds GetRepoRoot API to locate a repository root for a given path. |
| src/ImageBuilder/GitService.cs | Implements GetRepoRoot by delegating to GitHelper. |
| src/ImageBuilder/GitHelper.cs | Refactors commit SHA retrieval to use a shared repo-root discovery routine; supports .git as file or directory. |
| src/ImageBuilder/Commands/BuildCommand.cs | Reworks the build loop to push per-platform and attach an OCI referrer artifact with annotations after push. |
| src/ImageBuilder/Commands/Build/ImageArtifactDetailsExtensions.cs | Adds helper to enumerate all PlatformData entries from ImageArtifactDetails. |
| src/ImageBuilder.Tests/BuildCommandTests.cs | Adds tests covering attaching/skipping the metadata referrer and updates existing mocks for new git API. |
| string? digest = null; | ||
| for (int attempt = 0; attempt <= RetryHelper.MaxRetries && digest is null; attempt++) | ||
| { | ||
| digest = await imageDigestCache.GetLocalImageDigestAsync(tag.FullyQualifiedName, Options.IsDryRun); | ||
| } |
| internal static class ImageArtifactDetailsExtensions | ||
| { | ||
| /// <summary> | ||
| /// Enumerates all platform data entries from image-info repo and image groups. |
Part of #1602.
This PR attaches ImageBuilder build metadata to each newly built and pushed platform image as an OCI referrer artifact. The metadata is stored as annotations on the referrer manifest rather than as Docker image labels, so it does not get inherited by downstream images.
The referrer uses this artifact type:
application/vnd.microsoft.imagebuilder.image-info.v1vnd.microsoft.imagebuilder.sourcehttps://github.com/dotnet/docker-toolsvnd.microsoft.imagebuilder.revisionc0ffeec0ffeec0ffee...vnd.microsoft.imagebuilder.base.namemcr.microsoft.com/dotnet/runtime:9.0-azurelinux3.0vnd.microsoft.imagebuilder.base.digestsha256:abcdef123456...vnd.microsoft.imagebuilder.dockerfilesrc/Dockerfile.linuxThe Dockerfile annotation contains the path to the Dockerfile that the image was built from, relative to the repo root. This maintains parity with what's tracked in ImageArtifactDetails/image-info.json without baking the metadata into the image config.
Change to the build command's execution flow
While the git diff doesn't show it nicely, this PR rearranges the steps in the build command. Previously, there were multiple loops over all images. Now, each image is pushed immediately after it is built so ImageBuilder can attach the metadata referrer to the exact pushed image digest. Here is a high-level before/after comparison:
Before:
After:
Note: referrer annotations are not inherited by downstream images.